package com.taskinception;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.wifi.WpsInfo;
import android.net.wifi.p2p.WifiP2pConfig;
import android.net.wifi.p2p.WifiP2pDevice;
import android.net.wifi.p2p.WifiP2pDeviceList;
import android.net.wifi.p2p.WifiP2pInfo;
import android.net.wifi.p2p.WifiP2pManager;
import android.net.wifi.p2p.WifiP2pManager.ActionListener;
import android.net.wifi.p2p.WifiP2pManager.Channel;
import android.net.wifi.p2p.WifiP2pManager.ChannelListener;
import android.net.wifi.p2p.WifiP2pManager.ConnectionInfoListener;
import android.net.wifi.p2p.WifiP2pManager.PeerListListener;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import com.taskinception.model.Task;
import com.taskinception.networking.FileTransferService;
import com.taskinception.networking.TaskInceptionBroadcastReceiver;
import com.taskinception.serialization.XMLParser;
import com.taskinception.utils.Alert;
import com.taskinception.utils.TaskUtils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

/**
 * An activity that uses WiFi Direct APIs to discover and connect with available
 * devices. WiFi Direct APIs are asynchronous and rely on callback mechanism
 * using interfaces to notify the application of operation success or failure.
 * The application should also register a BroadcastReceiver for notification of
 * WiFi state related events.
 */
public class WiFiDirectActivity extends Activity implements ChannelListener,
		PeerListListener, ConnectionInfoListener {
	public static final String TAG = "WiFiDirect";
	public static final String TASK_ID = "task_id";
	public static final String MODE = "mode";
	
	private long taskID = -1;
	
	private WifiP2pManager manager;
	private Channel channel;
	
	private boolean isWifiP2pEnabled = false;
	private boolean retryChannel = false;

	private final IntentFilter intentFilter = new IntentFilter();
	private BroadcastReceiver receiver = null;

	private List<WifiP2pDevice> peers = new ArrayList<WifiP2pDevice>();
	private WiFiPeerListAdapter listAdapter;
	
	private WifiP2pDevice thisDevice;
	private WifiP2pDevice targetDevice;
	private WifiP2pInfo info;
	
	private ProgressDialog progressDialog = null;
	private ListView listView;
	private View connectDetailsView;
	private Button connectButton;
	private Button sendButton;
	private Button backButton;
	private Button disconnectButton;
	
	private TextView myNameTextView;
	private TextView myStatusTextView;
	private TextView targetNameTextView;
	private TextView targetStatusTextView;
	private TextView targetAddressTextView;
	
	private String myNameString;
	private String myStatusString;
	private String targetNameString;
	private String targetStatusString;
	private String targetAddressString;
	
	private int myRole;
	private static final int UNDETERMINED = 0;
	private static final int SERVER = 1;
	private static final int CLIENT = 2;
	
	private int myConnectionState;
	private static final int DEFAULT = 0;
	private static final int TARGET_SELECTED = 1;
	private static final int CONNECTING = 2;
	private static final int CONNECTED = 3;
	private static final int SENDING = 4;
	private static final int SENT = 5;
	
	private int mode;
	private static final int MODE_SHARE = 0;
	private static final int MODE_LISTEN = 1;

	/**
	 * @param isWifiP2pEnabled
	 *            the isWifiP2pEnabled to set
	 */
	public void setIsWifiP2pEnabled(boolean isWifiP2pEnabled) {
		this.isWifiP2pEnabled = isWifiP2pEnabled;
	}

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.wifi_direct);

		//get the taskID and mode in which this instance operates
		taskID = getIntent().getLongExtra(TASK_ID, 0);
		if (getIntent().getStringExtra(MODE).equals("listen")) {
			mode = MODE_LISTEN;
		}
		
		//set the broadcast which are we interested in
		intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
		intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
		intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
		intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);

		manager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
		channel = manager.initialize(this, getMainLooper(), null);

		connectDetailsView = findViewById(R.id.wifi_direct_connect_details);
		connectButton = (Button) findViewById(R.id.wifi_direct_connect_button);
		sendButton = (Button) findViewById(R.id.wifi_direct_send_button);
		backButton = (Button) findViewById(R.id.wifi_direct_back_button);
		disconnectButton = (Button) findViewById(R.id.wifi_direct_disconnect_button);
		myNameTextView = (TextView) findViewById(R.id.wifi_direct_my_name);
		myStatusTextView = (TextView) findViewById(R.id.wifi_direct_my_state);
		targetNameTextView = (TextView) findViewById(R.id.wifi_direct_target_name);
		targetStatusTextView = (TextView) findViewById(R.id.wifi_direct_target_state);
		targetAddressTextView = (TextView) findViewById(R.id.wifi_direct_target_address);
		listView = (ListView) findViewById(R.id.wifi_direct_list_view);
		
		initConnectionButtons();

		listAdapter = new WiFiPeerListAdapter(this, R.layout.wifi_direct_row_devices, peers);
		listView.setAdapter(listAdapter);
		listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
			@Override
			public void onItemClick(AdapterView l, View v, int position, long id) {
				WifiP2pDevice device = (WifiP2pDevice) listAdapter.getItem(position);
				if (mode == MODE_SHARE) {
					showTargetDetails(device);
				}
			}
		});
		
		myConnectionState = DEFAULT;
		updateGUI();
	}

	/** register the BroadcastReceiver with the intent values to be matched */
	@Override
	public void onResume() {
		super.onResume();
		receiver = new TaskInceptionBroadcastReceiver(manager, channel, this);
		registerReceiver(receiver, intentFilter);
	}

	@Override
	public void onPause() {
		super.onPause();
		unregisterReceiver(receiver);
	}

	/**
	 * Remove all peers and clear all fields. This is called on
	 * BroadcastReceiver receiving a state change event.
	 */
	public void resetData() {
		peers.clear();
	}

	/**
	 * Adds menu to activity.
	 * @param item item
	 */
	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		MenuInflater inflater = getMenuInflater();
		inflater.inflate(R.menu.wifi_direct_action_items, menu);
		return true;
	}

	/**
	 * Handles touch on menu buttons. In case of options it opens
	 * wifi settings activity and in case of refresh it sends request
	 * to discover available peers.
	 * @param item item
	 */
	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
		case R.id.atn_direct_enable:
			if (manager != null && channel != null) {

				// Since this is the system wireless settings activity, it's
				// not going to send us a result. We will be notified by
				// WiFiDeviceBroadcastReceiver instead.

				startActivity(new Intent(Settings.ACTION_WIRELESS_SETTINGS));
			} else {
				Log.e(TAG, "channel or manager is null");
			}
			return true;

		case R.id.atn_direct_discover:
			if (!isWifiP2pEnabled) {
				Toast.makeText(WiFiDirectActivity.this,
						R.string.wifi_direct_p2p_off_warning,
						Toast.LENGTH_SHORT).show();
				return true;
			}
			
			if (thisDevice.status == WifiP2pDevice.CONNECTED || thisDevice.status == WifiP2pDevice.INVITED) {
				disconnect();
			}
			
			Log.e(TAG, getDeviceStatus(thisDevice.status));
			
			updateGUI();
			
			onInitiateDiscovery();
			manager.discoverPeers(channel, new WifiP2pManager.ActionListener() {
				@Override
				public void onSuccess() {
					Toast.makeText(WiFiDirectActivity.this,
							getResources().getString(R.string.wifi_direct_discovery_initiated), Toast.LENGTH_SHORT).show();
				}

				@Override
				public void onFailure(int reasonCode) {
					Toast.makeText(WiFiDirectActivity.this,
							getResources().getString(R.string.wifi_direct_discovery_failed) + reasonCode,
							Toast.LENGTH_SHORT).show();
					progressDialog.dismiss();
				}
			});
			return true;
		default:
			return super.onOptionsItemSelected(item);
		}
	}

	/**
	 * Sends request to connect to address specified in config.
	 * We will be notified about result by broadcast.
	 * @param config config with info about connection
	 */
	public void connect(WifiP2pConfig config) {
		Log.d(TAG, "connect");
		manager.connect(channel, config, new ActionListener() {
			@Override
			public void onSuccess() {
				Log.d(TAG, "connect: onSuccess");
			}

			@Override
			public void onFailure(int reason) {
				Log.e(TAG, "connect: onFailure");
				Toast.makeText(WiFiDirectActivity.this,
						getResources().getString(R.string.wifi_direct_connect_failed) + reason, Toast.LENGTH_SHORT)
						.show();
			}
		});
	}

	/**
	 * Disconnects from currently connected group.
	 */
	public void disconnect() {
		Log.d(TAG, "disconnect");
		manager.removeGroup(channel, new ActionListener() {
			@Override
			public void onFailure(int reasonCode) {
				Log.d(TAG, "Disconnect failed. Reason :" + reasonCode);
			}

			@Override
			public void onSuccess() {
			}
		});
		myConnectionState = TARGET_SELECTED;
		updateGUI();
	}

	/**
	 * If channel is lost, tries to reconnect it and if also then it isn't working, we are screwed.
	 */
	@Override
	public void onChannelDisconnected() {
		Log.e(TAG, "onChannelDisconnected");
		if (manager != null && !retryChannel) {
			Toast.makeText(this, getResources().getString(R.string.wifi_direct_channel_lost1),
					Toast.LENGTH_LONG).show();
			resetData();
			retryChannel = true;
			manager.initialize(this, getMainLooper(), this);
		} else {
			Toast.makeText(
					this,
					getResources().getString(R.string.wifi_direct_channel_lost2),
					Toast.LENGTH_LONG).show();
		}
	}

	/**
	 * If device is connected, it disconnects it and if it is just connecting,
	 * it cancels the connection.
	 */
	public void cancelDisconnect() {
		Log.e(TAG, "cancelDisconnect");
		/*
		 * A cancel abort request by user. Disconnect i.e. removeGroup if
		 * already connected. Else, request WifiP2pManager to abort the ongoing
		 * request
		 */
		if (manager != null) {
			if (thisDevice == null
					|| thisDevice.status == WifiP2pDevice.CONNECTED) {
				disconnect();
			} else if (thisDevice.status == WifiP2pDevice.AVAILABLE
					|| thisDevice.status == WifiP2pDevice.INVITED) {

				manager.cancelConnect(channel, new ActionListener() {
					@Override
					public void onSuccess() {
						Log.e(TAG, "cancelDisconnect: onSuccess");
					}

					@Override
					public void onFailure(int reasonCode) {
						Log.e(TAG, "cancelDisconnect: onFailure");
					}
				});
			}
		}

	}

	/**
	 * Updates the ListView with peers. This method is called by broadcast.
	 * @param peerList list of peers
	 */
	@Override
	public void onPeersAvailable(WifiP2pDeviceList peerList) {
		updateGUI();
		Log.e(TAG, "onPeersAvailable");
		if (progressDialog != null && progressDialog.isShowing()) {
			progressDialog.dismiss();
		}
		peers.clear();
		peers.addAll(peerList.getDeviceList());
		listAdapter.notifyDataSetChanged();
		if (peers.size() == 0) {
			Log.d(WiFiDirectActivity.TAG, "No devices found");
			return;
		}

	}

	/**
	 * Updates GUI and displays progress dialog that peers are being discovered.
	 */
	public void onInitiateDiscovery() {
		updateGUI();
		Log.e(TAG, "onInitiateDiscovery");
		if (progressDialog != null && progressDialog.isShowing()) {
			progressDialog.dismiss();
		}
		progressDialog = ProgressDialog.show(this, getResources().getString(R.string.wifi_direct_press_back_to_cancel),
				getResources().getString(R.string.wifi_direct_finding_peers), true, true,
				new DialogInterface.OnCancelListener() {
					@Override
					public void onCancel(DialogInterface dialog) {

					}
				});
	}

	/**
	 * Determines the role (client/server) of curent device after successful connection.
	 * If we are server, FileServerAsyncTask is run to accept connections.
	 * @param info info about connection
	 */
	@Override
	public void onConnectionInfoAvailable(final WifiP2pInfo info) {
		Log.e(TAG, "onConnectionInfoAvailable");
		if (progressDialog != null && progressDialog.isShowing()) {
			progressDialog.dismiss();
		}
		this.info = info;
		// After the group negotiation, we assign the group owner as the file
		// server. The file server is single threaded, single connection server
		// socket.
		if (info.groupFormed && info.isGroupOwner) {
			myRole = SERVER;
			new FileServerAsyncTask(this).execute();
			
		} else if (info.groupFormed) {
			myRole = CLIENT;
		}
		
		myConnectionState = CONNECTED;
		updateGUI();
	}

	/**
	 * Updates name and status for this device.
	 * @param device WifiP2pDevice object
	 */
	public void updateThisDevice(WifiP2pDevice device) {
		Log.e(TAG, "updateThisDevice: new state: " + getDeviceStatus(device.status));
		this.thisDevice = device;
		
		myNameString = device.deviceName;
		myStatusString = getDeviceStatus(device.status);
		updateGUI();
	}
	
	/**
	 * Updates name, status and address of peer selected in ListView.
	 * @param device selected device
	 */
	public void showTargetDetails(WifiP2pDevice device) {
		Log.e(TAG, "showDetails");
		targetDevice = device;
		
		targetNameString = device.deviceName;
		targetStatusString = getDeviceStatus(device.status);
		targetAddressString = device.deviceAddress;
		
		myConnectionState = TARGET_SELECTED;
		updateGUI();
	}

	/**
	 * Handles received XML and asks user if he wants to merge them.
	 * @param data
	 */
	public void dataReceived(String data) {
		final Task rootTask = TIApplication.getInstance().getRootTask();
		final Task newTask = XMLParser.getTaskFromXML(data);

		Alert.confirm(WiFiDirectActivity.this, getResources().getString(R.string.wifi_direct_task_received),
				getResources().getString(R.string.wifi_direct_wanna_merge),
				new OnClickListener() {
					@Override
					public void onClick(DialogInterface dialog, int which) {
						TaskUtils.mergeTaskTrees(newTask, rootTask);
					}
				}, null);
	}
	
	
	/**
	 * Returns textual representation of device status.
	 * @param deviceStatus int value of status
	 * @return String with status
	 */
	private String getDeviceStatus(int deviceStatus) {
		switch (deviceStatus) {
		case WifiP2pDevice.AVAILABLE:
			return getResources().getString(R.string.wifi_direct_device_available);
		case WifiP2pDevice.INVITED:
			return getResources().getString(R.string.wifi_direct_device_invited);
		case WifiP2pDevice.CONNECTED:
			return getResources().getString(R.string.wifi_direct_device_connected);
		case WifiP2pDevice.FAILED:
			return getResources().getString(R.string.wifi_direct_device_failed);
		case WifiP2pDevice.UNAVAILABLE:
			return getResources().getString(R.string.wifi_direct_device_unavailable);
		default:
			return getResources().getString(R.string.wifi_direct_device_unknown);
		}
	}

	/**
	 * Updates visibility of GUI parts according to current status and role.
	 */
	private void updateGUI() {		
		if (myRole == SERVER) {
			connectDetailsView.setVisibility(View.GONE);
			return;
		}
		
		myNameTextView.setText(myNameString);
		myStatusTextView.setText(myStatusString);
		targetNameTextView.setText(targetNameString);
		targetStatusTextView.setText(targetStatusString);
		targetAddressTextView.setText(targetAddressString);
		
		switch (myConnectionState) {
		case DEFAULT:
			connectDetailsView.setVisibility(View.GONE);
			break;
		case TARGET_SELECTED:
			connectDetailsView.setVisibility(View.VISIBLE);
			connectButton.setVisibility(View.VISIBLE);
			connectButton.setClickable(true);
			sendButton.setVisibility(View.GONE);
			backButton.setVisibility(View.VISIBLE);
			disconnectButton.setVisibility(View.GONE);
			break;
		case CONNECTING:
			connectDetailsView.setVisibility(View.VISIBLE);
			connectButton.setVisibility(View.VISIBLE);
			connectButton.setClickable(false);
			sendButton.setVisibility(View.GONE);
			backButton.setVisibility(View.VISIBLE);
			disconnectButton.setVisibility(View.GONE);
			break;
		case CONNECTED:
			connectDetailsView.setVisibility(View.VISIBLE);
			connectButton.setVisibility(View.GONE);
			sendButton.setVisibility(View.VISIBLE);
			sendButton.setClickable(true);
			backButton.setVisibility(View.GONE);
			disconnectButton.setVisibility(View.VISIBLE);
			break;
		case SENDING:
			connectDetailsView.setVisibility(View.VISIBLE);
			connectButton.setVisibility(View.GONE);
			sendButton.setVisibility(View.VISIBLE);
			sendButton.setClickable(false);
			backButton.setVisibility(View.GONE);
			disconnectButton.setVisibility(View.VISIBLE);
			break;
		case SENT:
			connectDetailsView.setVisibility(View.VISIBLE);
			connectButton.setVisibility(View.GONE);
			sendButton.setVisibility(View.VISIBLE);
			sendButton.setClickable(false);
			backButton.setVisibility(View.GONE);
			disconnectButton.setVisibility(View.VISIBLE);
			break;
			
		default:
			Log.e(TAG, "updateGUI: unknown state!");
		}
	}
	
	/**
	 * Sets onClickListeners for buttons in details box in middle of screen.
	 */
	private void initConnectionButtons() {
		findViewById(R.id.wifi_direct_connect_button).setOnClickListener(
				new View.OnClickListener() {
					@Override
					public void onClick(View v) {
						WiFiDirectActivity.this.myConnectionState = CONNECTING;
						WiFiDirectActivity.this.updateGUI();
						//sets address to connect and lowest intent so the other device will be owner
						WifiP2pConfig config = new WifiP2pConfig();
						config.deviceAddress = targetDevice.deviceAddress;
						config.wps.setup = WpsInfo.PBC;
						config.groupOwnerIntent = 0;
						
						WiFiDirectActivity.this.connect(config);
					}
				});

		findViewById(R.id.wifi_direct_send_button).setOnClickListener(
				new View.OnClickListener() {
					@Override
					public void onClick(View v) {
						WiFiDirectActivity.this.myConnectionState = SENDING;
						WiFiDirectActivity.this.updateGUI();
						Intent serviceIntent = new Intent(WiFiDirectActivity.this,
								FileTransferService.class);
						serviceIntent.setAction(FileTransferService.ACTION_SEND_FILE);
						serviceIntent.putExtra(FileTransferService.EXTRAS_INPUT_STRING,
								getTaskXML(WiFiDirectActivity.this.taskID));
						serviceIntent.putExtra(FileTransferService.EXTRAS_GROUP_OWNER_ADDRESS,
								info.groupOwnerAddress.getHostAddress());
						serviceIntent.putExtra(FileTransferService.EXTRAS_GROUP_OWNER_PORT,	8988);
						WiFiDirectActivity.this.startService(serviceIntent);
					}
				});

		findViewById(R.id.wifi_direct_back_button).setOnClickListener(
				new View.OnClickListener() {
					@Override
					public void onClick(View v) {
						myConnectionState = DEFAULT;
						if (thisDevice.status == WifiP2pDevice.CONNECTED || thisDevice.status == WifiP2pDevice.INVITED) {
							disconnect();
						}
						findViewById(R.id.wifi_direct_connect_details)
								.setVisibility(View.INVISIBLE);
					}
				});
		
		findViewById(R.id.wifi_direct_disconnect_button).setOnClickListener(
				new View.OnClickListener() {
					@Override
					public void onClick(View v) {
						WiFiDirectActivity.this.disconnect();
						updateGUI();
					}
				});
	}
	
	/**
	 * Array adapter for ListFragment that maintains WifiP2pDevice list.
	 */
	private class WiFiPeerListAdapter extends ArrayAdapter<WifiP2pDevice> {

		private List<WifiP2pDevice> items;

		public WiFiPeerListAdapter(Context context, int textViewResourceId,
				List<WifiP2pDevice> objects) {
			super(context, textViewResourceId, objects);
			items = objects;

		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			View v = convertView;
			if (v == null) {
				LayoutInflater vi = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
				v = vi.inflate(R.layout.wifi_direct_row_devices, null);
			}
			WifiP2pDevice device = items.get(position);
			if (device != null) {
				TextView top = (TextView) v.findViewById(R.id.device_name);
				TextView bottom = (TextView) v
						.findViewById(R.id.device_details);
				if (top != null) {
					top.setText(device.deviceName);
				}
				if (bottom != null) {
					bottom.setText(getDeviceStatus(device.status));
				}
			}

			return v;

		}
	}

	/**
	 * A simple server socket that accepts connection and writes some data on
	 * the stream.
	 */
	public static class FileServerAsyncTask extends AsyncTask<Void, Void, String> {
		
		private Context context;

		public FileServerAsyncTask(Context context) {
			this.context = context;
		}

		@Override
		protected String doInBackground(Void... params) {
			try {
				ServerSocket serverSocket = new ServerSocket(8988);
				Log.d(WiFiDirectActivity.TAG, "Server: Socket opened");
				Socket client = serverSocket.accept();
				Log.d(WiFiDirectActivity.TAG, "Server: connection done");

				InputStream inputstream = client.getInputStream();
				OutputStream outputStream = new ByteArrayOutputStream();

				copyFile(inputstream, outputStream);

				String newXMLString = outputStream.toString();
				serverSocket.close();
//				Log.e("FileServerAsyncTask", "Received: " + newXMLString);
				return newXMLString;
			} catch (IOException e) {
				Log.e(WiFiDirectActivity.TAG, e.getMessage());
				return null;
			}
		}

		@Override
		protected void onPostExecute(String result) {
			((WiFiDirectActivity)context).dataReceived(result);

		}

		@Override
		protected void onPreExecute() {
		}
	}

	/**
	 * Just coppies one stream to another...
	 * @param inputStream
	 * @param out
	 * @return
	 */
	public static boolean copyFile(InputStream inputStream, OutputStream out) {
		byte buf[] = new byte[1024];
		int len;
		try {
			while ((len = inputStream.read(buf)) != -1) {
				out.write(buf, 0, len);
			}
			out.close();
			inputStream.close();
		} catch (IOException e) {
			Log.d(WiFiDirectActivity.TAG, e.toString());
			return false;
		}
		return true;
	}

	/**
	 * Calls methods to create XML from task tree.
	 * @param id id of selected task
	 * @return String with XML
	 */
	public String getTaskXML(long id) {
		Task task = TIApplication.getInstance().getTask(id);
		Task rootTask = TIApplication.getInstance().getRootTask();
		String xml = XMLParser.getXMLfromTask(task, rootTask);

		return xml;
	}

}